package com.qtrue.thread3;

/**
 * @Package: com.qtrue.thread3
 * @ClassName: ProductExample
 * @Author: xiaoBao
 * @CreateTime: 2022-05-14 10:53
 * @Description:线程通信的应用：生产者/消费者问题
 *
 *  生产者（Producer）将产品交给店员（Clerk），而消费者（Customer）从店员处取走产品，
 *  店员一次只能持有固定数量的产品（比如：20），如果生产者试图生产更多的产品，店员会叫生
 *  产者停一下，如果店中有空位放产品了再通知生产者继续生产；如果店中没有产品了，店员会告
 *  诉消费者等一下，如果店中有产品了再通知消费者来取走产品。
 *
 *  问题分析：
 *      1. 是否有多线程问题？ 是，生产者线程，消费者线程
 *      2. 是否有共享数据？ 是，店员（或产品）
 *      3. 如何解决线程安全问题？ 使用同步机制，有三种方法
 *      4. 是否涉及到线程的通信？ 是。
 */
public class ProductExample {
    public static void main(String[] args) {
        // 实例化店员类
        Clerk clerk = new Clerk();

        // 实例化生产者线程类
        Producer p1 = new Producer(clerk);
        // 设置线程名称
        p1.setName("生产者1");

        // 实例化消费者线程类
        Costmer cost1 = new Costmer(clerk);
        Costmer cost2 = new Costmer(clerk);
        // 设置线程名称
        cost1.setName("客户1");
        cost2.setName("客户2");

        // 启动线程
        p1.start();
        cost1.start();
        cost2.start();

    }
}

// 店员
class Clerk {
    // 产品数量
    private int productCount = 0;

    /*
        下面两个生产产品和消费产品的方法，都操作着共享数据（productCount），因此将他们声明为同步方法的原因有以下几点：
            1. 两个同步方法的同步监视器都为当前类对象（Clerk），为了保证多个线程使用的是同一把锁
            2. 一个时间段内只能有一个线程来操作共享数据。
            3. 当某个线程加入等待队列释放同步监视器以后，其它线程才有机会去拿到同步监视器操作共享数据。
            4. 可在适当的位置加入wait()和notify()方法：将满足自定义条件的线程加入等待队列或唤醒正在等待的线程。
                例如：程序第一次启动，会有生产者和消费者两个线程去抢同步监视器，操作共享数据。
                    如果最先抢到同步监视器的线程为消费产品的线程，此时产品数量为0，不满足消费条件，则线程加入等待队列并释放同步监视器，
                    然后此时生产产品的线程抢到同步监视器，开始操作共享数据生产产品，每生产一个产品就调用一次notify()唤醒正在等待队列的线程，
                    当产品数量不为0时，消费者线程也就满足消费条件了，而消费者每次消费一个产品也可以调用一次notify()唤醒正在等待队列的线程。
                    最后，无论是哪个线程先拿到同步监视器，当不满足自定义条件时都会释放同步监视器，让另一个线程来操作共享数据，
                    因此达到了线程安全的情况，两个一同加入到抢占同步监视器的队列来操作共享数据，正常地生产和消费产品。
    */

    // 生产产品（声明为同步方法：为了保证一个时间段内只能有一个线程对共享数据进行操作）
    public synchronized void produceProduct() { // 同步监视器：this（当前类对象）
        // 判断产品数量是否小于20个，满足则生产产品。
        if(productCount < 20){
            productCount++; // 首先每次生产都对产品数量进行运算，再说明生产的第几个产品
            System.out.println(Thread.currentThread().getName() + "：开始生产第" + productCount + "个产品....");

            // 只要生产者生产了一个产品，就可以唤醒正在等待队列的线程了（此时最开始第一次唤醒的线程会是消费产品的线程）
            notify();
        }else { // 反之则让当前生产者停止生产（此时将当前线程加入等待的队列）
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    // 消费产品（声明为同步方法：为了保证一个时间段内只能有一个线程对共享数据进行操作）
    public synchronized void costmerProduct() { // 同步监视器：this（当前类对象）
        // 判断产品数量是否大于0个，满足则消费产品。
        if(productCount > 0){
            System.out.println(Thread.currentThread().getName() + "：开始消费第" + productCount + "个产品....");
            productCount--; // 首先说明消费的是第几个产品，再对产品数量进行运算

            // 只要消费者消费了一个产品，就可以唤醒正在等待队列的线程了（此时唤醒的线程会是生产产品的线程）
            notify();
        }else {// 反之则让当前消费者停止消费（此时将当前线程加入等待的队列）
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

// 生产者
class Producer extends Thread{
    // 因为多个生产者共用一个店员，引入Clerk店员类。
    // 这里的对象属性也可以使用static来修饰，这样就不需要声明构造器在当前类实例化时传入参数了。
    private Clerk clerk;

    //声明一个构造器，在线程创建时传入同一个店员的对象。
    public Producer(Clerk clerk){
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "：开始生产产品....");

        while (true){
            try {
                Thread.sleep(10);// 线程休眠：时间越短生产越快，不休眠更快。
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            // 生产商品
            clerk.produceProduct();
        }
    }
}

// 消费者
class Costmer extends Thread{
    // 因为多个消费者共用一个店员，引入Clerk店员类。
    private Clerk clerk;

    //声明一个构造器，在线程创建时传入同一个店员的对象。
    public Costmer(Clerk clerk){
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "：开始消费产品....");

        while (true){
            try {
                Thread.sleep(50);// 线程休眠：时间越长消费越慢，反正则越快。
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            // 消费产品
            clerk.costmerProduct();
        }
    }
}
